Para esta segunda práctica nos encontrábamos frente a un problema de riesgo de crédito, el cual permite predecir mediante probabilidad la posibilidad de incurrir en una pérdida debido a un incumplimiento de un futuro crédito que se desee brindar. Para este ejercicio, se contó con la base de datos loan_data_2007_2014.csv obtenida a través de kaggle. Esta contiene información perteneciente a usuarios entre los años 2007 y 2014 de lendingclub, una empresa que realiza préstamos digitales en Estados Unidos ( lendingclub ).
El objetivo principal de la práctica fue el de realizar un modelo de probabilidad el cual permitiese predecir la probabilidad de que un individuo incumpla sus obligaciones financieras en los siguientes 12 meses desde que se genere el crédito.
También se debía representar este mismo modelo con un Scorecard, el cual es un valor numérico que sirve para medir la solvencia de un individuo. De igual forma se debía analizar qué variables hacen más riesgosa a una persona. Y finalmente, se debía desarrollar una aplicación web que le permitiera a los usuario ver su calificación de scorecard, de acuerdo a sus características, y cómo se encuentra respecto al resto de la población.
import pandas as pd
import numpy as np
import seaborn as sns
from scipy.stats import chi2_contingency
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import plotly.express as px
from sklearn.naive_bayes import GaussianNB
from tqdm import tqdm
from plotly.subplots import make_subplots
import plotly.graph_objects as go
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
mes_name=['Dec', 'Nov', 'Oct', 'Sep', 'Aug', 'Jul', 'Jun', 'May', 'Apr', 'Mar', 'Feb', 'Jan']
mes_num=list(pd.Series(list((13-np.arange(1,13)))).astype("str"))
def format_replace(string_): # ajust formato de fecha
for i in range(0,12):
string_=str(string_).replace(mes_name[i],mes_num[i] )
return string_
def dummy_creation(df_, columns_list): # creacion de variables dummy
df_dummies = []
for col in columns_list:
df_dummies.append(pd.get_dummies(df_[col], prefix = col, prefix_sep = ':',drop_first=True))
df_dummies = pd.concat(df_dummies, axis = 1)
df_ = pd.concat([df_, df_dummies], axis = 1)
df_=df_.drop(labels=columns_list,axis=1)
return df_
def plot_barplot(df_temp, x, y,nrow,ncol,fig_temp,show_leg):
df1 = df_temp.groupby(x)[y].value_counts(normalize=True)
df1 = df1.mul(100)
df1 = df1.rename('percent').reset_index()
df_temp=df1[df1[y]==1].copy()
fig_temp.add_trace(go.Bar(x=df_temp[x],
y=df_temp["percent"],
name="1",
marker_color='rgb(55, 83, 109)',
hovertext =(x,"percent" ),
showlegend=show_leg
), row=nrow,col=ncol)
df_temp=df1[df1[y]==0].copy()
fig_temp.add_trace(go.Bar(x=df_temp[x],
y=df_temp["percent"],
name="0",
marker_color='rgb(26, 118, 255)',
hovertext =(x,"percent" ),
showlegend=show_leg
),row=nrow,col=ncol)
fig_temp.update_yaxes(range = [0,100])
return fig_temp
df=pd.read_csv("loan_data_2007_2014.csv")
## <string>:1: DtypeWarning:
##
## Columns (19) have mixed types. Specify dtype option on import or set low_memory=False.
La base de datos loan_data_2007_2014.csv cuenta con 74 columnas y 466285 registros.
Para la creación del modelo se tienen:
issue_d: El mes en que se financió el préstamo (mes-año).
last_pymnt_d: El último mes de pago fue recibido.
loan_satus: Esta sera la variable objetivo, cuenta con 9 categorías que clasifican el ultimo estado registrado.
Como el objetivo es crear un modelo para predecir si al cabo de 12 meses que se origina el credíto (issue_d) el usuario incumple sus obligaciones financieras, luego de analizar las variables fecha registradas se crea month_last: meses que han pasado desde el ultimo pago, que es la diferencia (last_pymnt_d-issue_d ) esto nos dará informción del tiempo que pago el usuario y con la variable loan_status se podrá saber si el usuario incumple entre el tiempo de interés (antes de 12 meses).
Se crea las variables.
#results="asis"
table_frec=pd.DataFrame(df["loan_status"].value_counts())
status_mora=['Charged Off', 'Default', 'Late (31-120 days)','Does not meet the credit policy. Status:Charged Off']
table_frec["good_status"]=1
filtro=pd.Series(table_frec.index).isin( status_mora)
table_frec.loc[list(filtro), "good_status"]=0
table_frec=table_frec.reset_index()
table_frec.columns=["loan_status", "Frec", "good_status" ]
df_temp=table_frec[["loan_status", "good_status","Frec" ]]
df["good_status"]=1
df.loc[df["loan_status"].isin(status_mora),"good_status"]=0
| loan_status | good_status | Frec |
|---|---|---|
| Current | 1 | 224226 |
| Fully Paid | 1 | 184739 |
| Charged Off | 0 | 42475 |
| Late (31-120 days) | 0 | 6900 |
| In Grace Period | 1 | 3146 |
| Does not meet the credit policy. Status:Fully Paid | 1 | 1988 |
| Late (16-30 days) | 1 | 1218 |
| Default | 0 | 832 |
| Does not meet the credit policy. Status:Charged Off | 0 | 761 |
df[ 'issue_d']=pd.to_datetime(df['issue_d'], format = "%m-%y")
df['last_pymnt_d']=pd.to_datetime(df['last_pymnt_d'], format = "%m-%y")
df["month_last"]= ((df.last_pymnt_d - df.issue_d)/np.timedelta64(1, 'M'))
df['target_time']=0
df.loc[df["month_last"]<=12,'target_time']=1
El modelo general tendra la estructura:
\[ P(\text{good_status=1} )= f(\text{month_last}, {X },\theta ) \] Donde month_last define el tiempo en que queremos predecir, \(X\) es un vector de variables que puedan afectar la probabilidad y \(\theta\) son los parámetros que puede contener el modelo.
Asumiendo que se tolera al menos un 20 % de valores NA en los datos de las y omitiendo las columnas de identificación se cuenta con:
drop_columns=["id", "member_id", "url", "title"]
df=df.drop(labels=drop_columns,axis=1 )
total_na=df.isna().sum()
filtro=total_na< df.shape[0]*0.2
total_na=total_na[filtro]
result=pd.DataFrame({"Variables":["Menos del 20% NA"," Mas del 20% NA"],
"Total variables":[total_na.shape[0], 70-total_na.shape[0] ]} )
| Variables | Total variables |
|---|---|
| Menos del 20% NA | 51 |
| Mas del 20% NA | 19 |
Se omiten 22 variables por su alto porcentaje de valores faltantes, aunque 20% de valores faltantes es una cantidad alta, existen variables importantes que contienen alta cantidad de valores faltantes que se muestran a continuación.
vars_=total_na.sort_values(ascending=False).head()
result=pd.DataFrame({"Variables":vars_.index,"Descripción":[" Total crédito rotativo alto entre límite de crédito. ","Saldo corriente en todas las cuentas ", " Montos totales de cobro adeudados. ", "Tipo de trabajo.","Años en el trabajo " ], "Total NA":vars_ })
result=result.reset_index()
| index | Variables | Descripción | Total NA |
|---|---|---|---|
| total_rev_hi_lim | total_rev_hi_lim | Total crédito rotativo alto entre límite de crédito. | 70276 |
| tot_cur_bal | tot_cur_bal | Saldo corriente en todas las cuentas | 70276 |
| tot_coll_amt | tot_coll_amt | Montos totales de cobro adeudados. | 70276 |
| emp_title | emp_title | Tipo de trabajo. | 27588 |
| emp_length | emp_length | Años en el trabajo | 21008 |
De estas variables puede ser dificil que el usuario obtenga total_rev_hi_lim, emp_title tiene muchas categorías.
Es importante identificar que variables puede dar un usuario al momento del registro, pues existen variables donde se obtienen la información al pasar el tiempo o un usuario no puede identificar.
| funded_amnt_inv | grade | sub_grade | emp_title |
| verification_status | zip_code | addr_state | dti |
| delinq_2yrs | inq_last_6mths | revol_bal | revol_util |
| total_acc | out_prncp | out_prncp_inv | out_prncp_inv |
| total_pymnt | total_rec_prncp | total_rec_int | total_rec_late_fee |
| recoveries | collection_recovery_fee | last_pymnt_amnt | last_credit_pull_d |
| collections_12_mths_ex_med | policy_code | tot_coll_amt | total_rev_hi_lim |
Las variables en la tabla no se tienen en cuenta porque son medidas que son proporcionadas por LC, información al pasar el tiempo después del prestamo o son extraidas de un externo, por ende, las variables a considerar como influyentes en el incumplimiento de las finanzas son:
| Variables | Descripción |
|---|---|
| loan_amnt | El monto indicado del préstamo solicitado por el prestatario. Si en algún momento, el departamento de crédito reduce el monto del préstamo, entonces se reflejará en este valor. |
| funded_amnt | El monto total comprometido con ese préstamo en ese momento. |
| term | El número de pagos del préstamo. Los valores son en meses y pueden ser 36 o 60. |
| int_rate | tasa de interés del préstamo. |
| installment | cuota El pago mensual adeudado por el prestatario si el préstamo se origina. |
| home_ownership | El estado de propiedad de la vivienda proporcionado por el prestatario durante el registro. Nuestros valores son |
| annual_inc | Los ingresos anuales autoinformados proporcionados por el prestatario durante el registro. |
| earliest_cr_line | El mes en que se abrió la primera línea de crédito reportada del prestatario. |
| open_acc | El número de líneas de crédito abiertas en el archivo de crédito del prestatario. |
| pub_rec | numero de derogatory public records. |
| acc_now_delinq | El número de cuentas en las que el prestatario está ahora en mora. |
| purpose | Razón por la que se hace el prestamo. |
| tot_cur_bal | Saldo corriente total de todas las cuentas |
| emp_length | años trabajo |
| initial_list_status | El estado inicial de listado del préstamo. Los valores posibles son – W, F |
| pymnt_plan | indica si se a establecido un plan de pago. |
En esta tabla se tienen las posibles variables para el modelo, con good_status y target_time.
variables_=pd.Series(variables_).apply(lambda x: x.replace("__","" ))
df["month_earliest_cr_line"]=((df.issue_d-df.earliest_cr_line )/np.timedelta64(1, 'M'))
df=df[ [*variables_, "good_status","target_time","month_earliest_cr_line" ]]
df=df[~df.isna().any(axis=1)]
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Cantidad":df.shape})
| Cantidad | |
|---|---|
| Filas | 377062 |
| Columnas | 19 |
Como resultado para el modelo se usara un data frame con estas dimesiones.
¿Cuál es la distribución de good_estatus?
tabla_1=df.good_status.value_counts(normalize=True)*100
tabla_2=df.good_status.value_counts()
tabla_3=df.target_time.value_counts(normalize=True)*100
tabla_4=df.target_time.value_counts()
table_final=pd.DataFrame({"good status":[1,0],"Frecuencia":tabla_2,
"Frec %":tabla_1, "target time":[0,1],
"Frecuencia ":tabla_4, "Frec % ":tabla_3 } )
| good status | Frecuencia | Frec % | target time | Frecuencia | Frec % |
|---|---|---|---|---|---|
| 1 | 37709 | 10 | 0 | 305925 | 81.13 |
| 0 | 339353 | 90 | 1 | 71137 | 18.87 |
La variable acc_now_delinq se puede transformar en 1 si tiene al menos una cuenta en mora, 0 sino tiene cunetas en mora. También la variable pub_rec se transforma 1 si tiene al menos un derogatory public records, 0 sino. Se tomo la decisión ya que son variables conteos donde no parece ser necesario. Como esta es la fecha en que se hizo su primer prestamo earliest_cr_line, se debe calcular los meses que han pasado desde que solicito el prestamo month_earliest_cr_line
df=df.drop(labels="earliest_cr_line",axis=1)
df["acc_now_delinq"]=np.where(df["acc_now_delinq"]>0, 1,0)
df["pub_rec"]=np.where(df["pub_rec"]>0,1,0)
df_temp=df.copy()
df_temp["acc_now_delinq"]=df_temp["acc_now_delinq"].astype("str")
df_temp["target_time"]=df_temp["target_time"].astype("str")
df_temp["pub_rec"]=df_temp["pub_rec"].astype("str")
X=df_temp.drop(labels="good_status",axis=1)
Y=df_temp["good_status"]
X_train_cat = X.select_dtypes(include = 'object').copy()
X_train_num = X.select_dtypes(include = 'number').copy()
# define an empty dictionary to store chi-squared test results
chi2_check = {}
# loop over each column in the training set to calculate chi-statistic with the target variable
for column in X_train_cat:
chi, p, dof, ex = chi2_contingency(pd.crosstab(Y, X_train_cat[column]))
chi2_check.setdefault('Feature',[]).append(column)
chi2_check.setdefault('p-value',[]).append(round(p, 10))
# convert the dictionary to a DF
chi2_result = pd.DataFrame(data = chi2_check)
chi2_result.sort_values(by = ['p-value'], ascending = True, ignore_index = True, inplace = True)
| Feature | p-value |
|---|---|
| term | 0.00 |
| home_ownership | 0.00 |
| purpose | 0.00 |
| emp_length | 0.00 |
| initial_list_status | 0.00 |
| target_time | 0.00 |
| pub_rec | 0.00 |
| pymnt_plan | 0.14 |
| acc_now_delinq | 0.51 |
Se realiza pruebas \(\chi^2\) para las variables categoricas en contraste con la variable good_status y se observa 8 variables con un p-valor pequeño, esto significa que estas variables pueden influir en el incumplimiento de las finanzas. Se excluye pymnt_plan debido a un p-valor > 0.05.
corrmat = X_train_num.corr()
sns.heatmap(corrmat)
Correlación entre variables númericas.
En la figura 1 se observa que hay 3 variables con una alta correlación entre si (\(\approx\) 1) que son: loan_amnt, funded_amnt, installment y según la table - 5 se opta por loan_amnt por ser el prestamo definitivo que dio LC.
vars_=['loan_amnt', 'int_rate', 'annual_inc',
'open_acc', 'tot_cur_bal',"month_earliest_cr_line"]
fig, axs = plt.subplots(ncols=3,nrows=2,figsize=(18, 7))
sns.boxplot(data=df, x="good_status", y=vars_[0], ax=axs[0, 0],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[1], ax=axs[0, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[2], ax=axs[0, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[3], ax=axs[1, 1],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[4], ax=axs[1, 2],showfliers = False)
sns.boxplot(data=df, x="good_status", y=vars_[5], ax=axs[1, 0],showfliers = False)
Box plot comparativos dado good_status
En la figura se observa que las variables con una tendencia de influir en good_status son int_rate, annual_inc pues aunque no se observa gran diferencia, parece existir una influencia.
Las variables a usar para crear el modelo son:
variables_final=list(chi2_result["Feature"].iloc[0:8])
variables_final=[*variables_final,*vars_,"good_status"]
result=np.array([*variables_final,""])
result.shape=(4,4)
result=pd.DataFrame(result)
result.columns=[""]*4
| term | home_ownership | purpose | emp_length |
| initial_list_status | target_time | pub_rec | pymnt_plan |
| loan_amnt | int_rate | annual_inc | open_acc |
| tot_cur_bal | month_earliest_cr_line | good_status |
Estas son las covariables que se tendrán en el modelo.
df.loc[df["home_ownership"] == "ANY","home_ownership"] = 'NONE'
var_cat=['term','pub_rec','home_ownership','initial_list_status','purpose','emp_length']
fig_ = make_subplots(rows=4, cols=2,subplot_titles =var_cat)
fig_ =plot_barplot(df, var_cat[0],"good_status", 1,1,fig_,True)
fig_ =plot_barplot(df, var_cat[1],"good_status", 1,2,fig_, True)
fig_ =plot_barplot(df, var_cat[2],"good_status", 2,1,fig_, True)
fig_ =plot_barplot(df, var_cat[3],"good_status", 2,2,fig_, True)
fig_ =plot_barplot(df, var_cat[4],"good_status", 3,1,fig_, True)
fig_ =plot_barplot(df, var_cat[5],"good_status", 3,2,fig_, True)
# fig_ =plot_barplot(df, var_cat[6],"good_status", 4,1,fig_, True)
fig_.update_layout(
title="Good status",
xaxis_tickfont_size=14,
yaxis=dict(
title='Distribution percent ',
titlefont_size=16,
tickfont_size=14,
),
legend=dict(
x=1,
y=1.0,
bgcolor='rgba(255, 255, 255, 0)',
bordercolor='rgba(255, 255, 255, 0)'
),
barmode='group',
bargap=0.15, # gap between bars of adjacent location coordinates.
bargroupgap=0.1, # gap between bars of the same location coordinate.
height=1000, width=900
)
Cuando el número de pagos es \(>\) 36 meses la probabilidad de incumplimiento es mayor, es decir, un usuario puede inclumir si tiene mas número de pagos al inicio del prestamo.
Si el usuario marca NONE o OTHER o RENT influye negativamente, es decir, aumenta la probabilidad de que incumpla sus obligaciones financieras.
Si el proposito del prestamo es small_business, house, weddlng aumenta la probabilidad de que incumpla sus obligaciones financieras.
A medida que el usuario tiene menor tiempo de trabajo, aumenta la probabilidad de que incumpla sus obligaciones financieras.
Se plantean diferentes modelos aplicando validación cruzada 80% prueba 20% test.
Luego de ensayar modelos se encontro que el valor de probabilidad de cambio mas adecuado es 0.8, es decir, si la probabilidad >=0.8 good_status 1, por el contrario 0.
df_modelo=df[variables_final].copy()
df_modelo.loc[df_modelo["home_ownership"] == "ANY","home_ownership"] = 'NONE'
X = df_modelo.drop('good_status', axis = 1).copy()
X=dummy_creation(X, ["term","home_ownership", "purpose",'initial_list_status','emp_length' ,"pymnt_plan"])
y = df_modelo['good_status'].copy()
# y= np.where(y==1,0,1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 421)#, stratify = y)
X_train, X_test = X_train.copy(), X_test.copy()
result=pd.DataFrame({"":["Filas", "Columnas"] ,"Trian":X_train.shape,"Test":X_test.shape })
| Trian | Test | |
|---|---|---|
| Filas | 301649 | 75413 |
| Columnas | 37 | 37 |
De las variables que se escogieron se probo eliminando variables y para el modelo las variables selecionadas son: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership.
var_=[
'target_time',
'pub_rec',
'loan_amnt',
'int_rate',
'open_acc',
'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
'home_ownership:RENT',
]
X_train_pca=X_train[var_]
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_]
# X_test_pca=PCA_5.transform(X_test)
clf = GaussianNB()
clf.fit(X_train_pca, y_train)
## GaussianNB()
prop_=0.8
predict_train_nb= clf.predict_proba(X_train_pca)[:,1]
predict_test_nb= clf.predict_proba(X_test_pca)[:,1]
predict_train_nb=np.where(predict_train_nb>=prop_,1,0 )
predict_test_nb=np.where(predict_test_nb>=prop_,1,0 )
tasa_acierto_1=accuracy_score(y_train, predict_train_nb)*100
tasa_acierto_2=accuracy_score(y_test,predict_test_nb )*100
result=pd.DataFrame({"Población":["Train", "Test"], "Aciertos": [tasa_acierto_1,tasa_acierto_2 ] })
errores_bayes=pd.concat([pd.crosstab(y_train,predict_train_nb ,margins=False, normalize=True, colnames=["predicción"], rownames=['Real']),
pd.crosstab(y_test,predict_test_nb , margins=False, normalize=True, colnames=["predicción"], rownames=['Real'])
],axis=1,keys=["Train", "Test"])
| Población | Aciertos |
|---|---|
| Train | 85.11 |
| Test | 85.20 |
Se observa una tasa de acierto homogenea entre los datos de prueba y entrenamiento.
Para este modelo se encontraron las variables apropiadas: target_time, pub_rec, loan_amnt, int_rate, open_acc, home_ownership, emp_length.
var_=[
'target_time',
'pub_rec',
'loan_amnt',
'int_rate',
'open_acc',
'home_ownership:NONE', 'home_ownership:OTHER', 'home_ownership:OWN',
'home_ownership:RENT',
'emp_length:10+ years', 'emp_length:2 years',
'emp_length:3 years', 'emp_length:4 years', 'emp_length:5 years',
'emp_length:6 years', 'emp_length:7 years', 'emp_length:8 years',
'emp_length:9 years', 'emp_length:< 1 year'
]
X_train_pca=X_train[var_]
# X_train_pca=PCA_5.transform(X_train)
X_test_pca=X_test[var_]
# X_test_pca=PCA_5.transform(X_test)
Modelo = LogisticRegression()#C=1e-09,class_weight="balanced",solver="sag")
Modelo.fit(X_train_pca, y_train)
## LogisticRegression()
prop_=0.8
predict_train_log= Modelo.predict_proba(X_train_pca)[:,1]
predict_test_log= Modelo.predict_proba(X_test_pca)[:,1]
predict_train_log=np.where(predict_train_log>=prop_,1,0 )
predict_test_log=np.where(predict_test_log>=prop_,1,0 )
tasa_acierto_1=accuracy_score(y_train, predict_train_log)*100
tasa_acierto_2=accuracy_score(y_test,predict_test_log )*100
result=pd.DataFrame({"Población":["Train", "Test"], "Aciertos": [tasa_acierto_1,tasa_acierto_2 ] })
errores_logistico=pd.concat([pd.crosstab(y_train,predict_train_log ,margins=False, normalize=True, colnames=["predicción"], rownames=['Real']),
pd.crosstab(y_test,predict_test_log , margins=False, normalize=True, colnames=["predicción"], rownames=['Real'])
],axis=1,keys=["Train", "Test"])
| Población | Aciertos |
|---|---|
| Train | 82.23 |
| Test | 82.45 |
Esto es menor al modelo de bayes, luego se deben analizar los tipos de errores en cada modelo.
| Train 0 | 1 | Test 0 | 1 | |
|---|---|---|---|---|
| 0 | 4.34 | 5.71 | 4.36 | 5.48 |
| 1 | 12.06 | 77.90 | 12.07 | 78.09 |
| Train 0 | 1 | Test 0 | 1 | |
|---|---|---|---|---|
| 0 | 4.26 | 5.78 | 4.18 | 5.66 |
| 1 | 9.11 | 80.85 | 9.14 | 81.02 |